﻿/*

========================================================================

	                        Light Prepass Renderer

		                        by Floris Groen

                         E-mail : fmfgroen@yahoo.com
                     Blog : www.florisgroen.blogspot.com
		      

========================================================================

*/

using LightPrePassRenderer.Engine.Interfaces;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace LightPrePassRenderer.Engine
{
    public class Renderer : IRenderer
    {
        /// <summary>
        /// Contains depth information for the scene.
        /// </summary>
        public RenderTarget2D Depth
        {
            get;
            private set;
        }

        /// <summary>
        /// Contains normals for the scene.
        /// </summary>
        public RenderTarget2D Normals
        {
            get;
            private set;
        }

        /// <summary>
        /// Light accumulation buffer.
        /// </summary>
        public RenderTarget2D Light
        {
            get;
            private set;
        }

        GraphicsDevice graphicsDevice;
        ContentManager content;
        GameServiceContainer services;

        Effect depthAndNormalsEffect;
        DepthAndNormalsMapping depthAndNormalsEffectMapping;

        Effect clearDepthAndNormalsEffect;
        Effect lightEffect;
        
        Effect compositeEffect;
        CompositeMapping compositeEffectMapping;

        ILightManager lightManager;
        IRenderManager renderManager;

#if DEBUG
        //Used to display buffers in debug mode.
        SpriteBatch spriteBatch;
#endif

        //Screen space quad used for directional light and clearing buffers.
        FullScreenQuad fullScreenQuad;

        //Bounding volume used for rendering point lights.
        Model sphere;

        //Camera frustum corners are used in shader to reconstruct position.
        Vector3[] worldFrustumCorners;
        Vector3[] viewFrustumCorners;
        Vector3[] farPlaneCorners;

        /// <summary>
        /// Offset for aligning texels to pixel due to weird DX9 sampling.
        /// </summary>
        public Vector2 HalfPixel
        {
            get;
            private set;
        }

        public Renderer(GraphicsDevice graphicsDevice, GameServiceContainer services, ContentManager content)
        {
            this.graphicsDevice = graphicsDevice;
            this.content = content;
            this.services = services;

            //We will pull the necessary managers from the service container.
            //Interfaces are used to make it easier to override the managers.
            lightManager = (ILightManager)services.GetService(typeof(ILightManager));
            renderManager = (IRenderManager)services.GetService(typeof(IRenderManager));
            
            worldFrustumCorners = new Vector3[8];
            viewFrustumCorners = new Vector3[8];
            farPlaneCorners = new Vector3[4];
        }

        /// <summary>
        /// Call this to initialize the renderer. Creates and loads necessary buffers and resources.
        /// </summary>
        public void Initialize()
        {
            //The half pixel will be used in effects to align texels to pixels.
            HalfPixel = new Vector2
            (
                0.5f / graphicsDevice.PresentationParameters.BackBufferWidth,
                0.5f / graphicsDevice.PresentationParameters.BackBufferHeight
            );

            //This will contain normalised view space depth for the scene.
            Depth = new RenderTarget2D
            (
                graphicsDevice,
                graphicsDevice.PresentationParameters.BackBufferWidth,
                graphicsDevice.PresentationParameters.BackBufferHeight,
                1,
                SurfaceFormat.Single,
                MultiSampleType.None,
                0,
                RenderTargetUsage.DiscardContents
            );

            //This will contain view space normals.
            Normals = new RenderTarget2D
            (
                graphicsDevice,
                graphicsDevice.PresentationParameters.BackBufferWidth,
                graphicsDevice.PresentationParameters.BackBufferHeight,
                1,
                SurfaceFormat.Color,
                MultiSampleType.None,
                0,
                RenderTargetUsage.DiscardContents
            );

            //We will render all our lights to this light accumulation buffer.
            Light = new RenderTarget2D
            (
                graphicsDevice,
                graphicsDevice.PresentationParameters.BackBufferWidth,
                graphicsDevice.PresentationParameters.BackBufferHeight,
                1,
                SurfaceFormat.Color,
                MultiSampleType.None,
                0,
                RenderTargetUsage.DiscardContents
            );

            //Load the effect used for writing depth and normals to the render targets.
            depthAndNormalsEffect = content.Load<Effect>("Effects\\DepthAndNormals");

            //This will map the parameters of the previous effect to the appropriate variables.
            depthAndNormalsEffectMapping = new DepthAndNormalsMapping(depthAndNormalsEffect, services);

            //This will be used for clearing the depth and normal buffers.
            clearDepthAndNormalsEffect = content.Load<Effect>("Effects\\ClearDepthAndNormals");

            //Effect for rendering directional and point lights.
            lightEffect = content.Load<Effect>("Effects\\Light");
            
            //This effect will be used for rendering the final scene.
            compositeEffect = content.Load<Effect>("Effects\\Composite");

            //Parameter mapping for the previous effect.
            compositeEffectMapping = new CompositeMapping(compositeEffect, services);

            //This sphere will be used as a light volume for point lights.
            sphere = content.Load<Model>("Models\\sphere");

#if DEBUG
            //Used for debugging purposes.
            spriteBatch = new SpriteBatch(graphicsDevice);
#endif

            //Rectangle that covers the whole screen. Used for directional lights
            //and clearing the depth and normal render targets,
            fullScreenQuad = new FullScreenQuad(graphicsDevice);
        }

        /// <summary>
        /// Call this to clean up after the renderer.
        /// </summary>
        public void Deinitialize()
        {
            Depth.Dispose();
            Normals.Dispose();
            Light.Dispose();

#if DEBUG
            spriteBatch.Dispose();
#endif
        }

        /// <summary>
        /// Draws the scene.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Draw(GameTime gameTime)
        {
            ClearFrame();

            graphicsDevice.SetRenderTarget(0, Depth);
            graphicsDevice.SetRenderTarget(1, Normals);

            ClearDepthAndNormals();
            DrawDepthAndNormals();

            graphicsDevice.SetRenderTarget(1, null);

            graphicsDevice.SetRenderTarget(0, Light);

            ClearLight();
            DrawLight();

            graphicsDevice.SetRenderTarget(0, null);

            DrawFrame();

#if DEBUG
            //When in debugging mode we will display the buffers.
            //That way we can see what's going on.
            spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);

            spriteBatch.Draw(Depth.GetTexture(), new Rectangle(0, 0, 300, 300), Color.White);
            spriteBatch.Draw(Normals.GetTexture(), new Rectangle(300, 0, 300, 300), Color.White);
            spriteBatch.Draw(Light.GetTexture(), new Rectangle(600, 0, 300, 300), Color.White);

            spriteBatch.End();
#endif

            //We empty the render queue when we're done with it.
            //Otherwise it would just get bigger and bigger when
            //more models are submitted for rendering.
            renderManager.ClearRenderQueue();
        }

        /// <summary>
        /// Uses full screen quad to clear depth and normal buffer.
        /// </summary>
        private void ClearDepthAndNormals()
        {
            clearDepthAndNormalsEffect.Begin(SaveStateMode.None);

            foreach (EffectPass pass in clearDepthAndNormalsEffect.Techniques[0].Passes)
            {
                pass.Begin();

                fullScreenQuad.Draw();

                pass.End();
            }

            clearDepthAndNormalsEffect.End();
        }

        /// <summary>
        /// Draws depth and normals.
        /// </summary>
        private void DrawDepthAndNormals()
        {
            depthAndNormalsEffectMapping.SetParametersPerFrame();

            depthAndNormalsEffect.Begin(SaveStateMode.None);
            foreach (EffectPass pass in depthAndNormalsEffect.Techniques[0].Passes)
            {
                pass.Begin();

                for (int i = 0; i < renderManager.QueueCount; i++)
                {
                    var entry = renderManager.GetRenderQueueItem(i);
                    depthAndNormalsEffectMapping.SetParametersPerObject(entry);
                    entry.ModelInstance.Draw(graphicsDevice);
                }

                pass.End();
            }
            depthAndNormalsEffect.End();
        }

        /// <summary>
        /// Clears the light buffer to white. We store our lights in a [1..0] range, that's why we don't clear to black.
        /// </summary>
        private void ClearLight()
        {
            graphicsDevice.Clear(ClearOptions.Target, Color.White, 1.0f, 1);
        }

        /// <summary>
        /// Draws all the lighting into the light buffer.
        /// </summary>
        private void DrawLight()
        {
            Matrix viewMatrix, projectionMatrix;

            renderManager.Camera.GetView(out viewMatrix);
            renderManager.Camera.GetProjection(out projectionMatrix);

            renderManager.Camera.BoundingFrustum.GetCorners(worldFrustumCorners);
            Vector3.Transform(worldFrustumCorners, ref viewMatrix, viewFrustumCorners);
            for (int i = 0; i < 4; i++)
                farPlaneCorners[i] = viewFrustumCorners[i + 4];

            lightEffect.Parameters["HalfPixel"].SetValue(HalfPixel);
            lightEffect.Parameters["Depth"].SetValue(Depth.GetTexture());
            lightEffect.Parameters["Normal"].SetValue(Normals.GetTexture());
            lightEffect.Parameters["FarPlane"].SetValue(renderManager.Camera.FarPlane);
            lightEffect.Parameters["CameraPosition"].SetValue(Vector3.Transform(renderManager.Camera.Position, viewMatrix));
            lightEffect.Parameters["FrustumCorners"].SetValue(farPlaneCorners);

            lightEffect.Begin(SaveStateMode.None);

            EffectPass directionalLightPass = lightEffect.Techniques[0].Passes[0];
            
            directionalLightPass.Begin();

            for (int i = 0; i < lightManager.DirectionalLightCount; i++)
            {
                DirectionalLight directionalLight = lightManager.GetDirectionalLight(i);

                lightEffect.Parameters["LightColor"].SetValue(directionalLight.Color.ToVector3());
                lightEffect.Parameters["LightIntensity"].SetValue(directionalLight.Intensity);
                lightEffect.Parameters["LightDirection"].SetValue(directionalLight.Direction);
                lightEffect.CommitChanges();

                fullScreenQuad.Draw();
            }

            directionalLightPass.End();


            for (int i = 0; i < lightManager.PointLightCount; i++)
            {
                graphicsDevice.Clear(ClearOptions.Stencil, Color.White, 1.0f, 1);

                PointLight pointLight = lightManager.GetPointLight(i);

                //We're multiplying by 1.5 because the bounding volumes seem to be a little too small;
                Matrix world = Matrix.CreateScale(pointLight.Radius) *
                                   Matrix.CreateTranslation(pointLight.Position);
                Matrix wv = world * viewMatrix;
                Matrix wvp = wv * projectionMatrix;
                lightEffect.Parameters["WorldView"].SetValue(wv);
                lightEffect.Parameters["WorldViewProjection"].SetValue(wvp);
                lightEffect.Parameters["LightPosition"].SetValue(Vector3.Transform(pointLight.Position, viewMatrix));
                lightEffect.Parameters["LightRadius"].SetValue(pointLight.Radius);
                lightEffect.Parameters["LightColor"].SetValue(pointLight.Color.ToVector3());
                lightEffect.Parameters["LightIntensity"].SetValue(pointLight.Intensity);
                lightEffect.CommitChanges();
                
                EffectPass pass = lightEffect.Techniques[0].Passes[1];

                pass.Begin();

                DrawPointLight();

                pass.End();

                pass = lightEffect.Techniques[0].Passes[2];

                pass.Begin();

                DrawPointLight();

                pass.End();
            }

            lightEffect.End();
        }

        /// <summary>
        /// Draws the bounding volume.
        /// </summary>
        private void DrawPointLight()
        {
            foreach (ModelMesh mesh in sphere.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    graphicsDevice.VertexDeclaration = meshPart.VertexDeclaration;

                    graphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer,
                    meshPart.StreamOffset, meshPart.VertexStride);

                    graphicsDevice.Indices = mesh.IndexBuffer;

                    graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                    meshPart.BaseVertex, 0, meshPart.NumVertices, meshPart.StartIndex,
                    meshPart.PrimitiveCount);
                }
            }
        }

        /// <summary>
        /// Clears the back buffer.
        /// </summary>
        private void ClearFrame()
        {
            graphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1.0f, 0);
        }

        /// <summary>
        /// Draws the final frame into the back buffer.
        /// </summary>
        private void DrawFrame()
        {
            compositeEffectMapping.SetParametersPerFrame();

            compositeEffect.Begin(SaveStateMode.None);
            foreach (EffectPass pass in compositeEffect.Techniques[0].Passes)
            {
                pass.Begin();

                for (int i = 0; i < renderManager.QueueCount; i++)
                {
                    var entry = renderManager.GetRenderQueueItem(i);
                    compositeEffectMapping.SetParametersPerObject(entry);
                    entry.ModelInstance.Draw(graphicsDevice);
                }

                pass.End();
            }
            compositeEffect.End();
        }
    }
}
